<?php

namespace App\Service\WeChat;


use Illuminate\Support\Facades\Config;

class MiniPayV3
{

    // 提现
    //   https://cloud.tencent.com/developer/article/2233513
    //   https://www.cnblogs.com/leaf-cq/p/14302927.html
    //   小程序 提现 v3 php


    // 小程序支付
    //   独立，不依赖于其他程序


    #######################################################################################

    // 参数
    // 回调密码：wpf1QP04eyOMFObzUI2pgCnuKIy5Vwlh
    /*protected $mini_appid = 'wx519c562524bb167f'; // 微信小程序 AppID
    protected $mini_key   = '1e9ff8308564ca2aa9a2a29b5b10a4ca';   // 微信小程序 key
    protected $mchid      = '1682898467';      // 微信支付商户号  1682898467
    protected $serial_no  = '7DC5F002FBA35D0B13420BAA49B8AFE7BF17C62B';      // 微信支付序列号*/


    // 参数
    protected $mini_appid;
    protected $mini_key;
    protected $mchid;
    protected $serial_no;
    protected $api3_key;

    // 初始化
    public function __construct($appId, $key, $mchid, $serial_no, $api3_key)
    {
        $this->mini_appid = $appId;
        $this->mini_key   = $key;
        $this->mchid      = $mchid;
        $this->serial_no  = $serial_no;
        $this->api3_key   = $api3_key;
    }

    // 制作小程序支付
    public function makeMiniPay($description, $trade_no, $openid, $money, $notify_url)
    {
        //请求参数(报文主体)
        $apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi';   //H5支付

        // 计算过期时间
        $dateTime = new \DateTime();
        $dateTime->setTimestamp(LARAVEL_TIME + 1800);  // 30分钟 后过期
        $dateTime->setTimezone(new \DateTimeZone('+08:00'));   // 设置时区：北京时间
        $expire = $dateTime->format(\DateTime::ATOM);  // 转换为ISO 8601格式   输出: 2018-06-08T10:34:56+08:00


        // 请求报文 与 header
        $body      = [
            'appid'        => $this->mini_appid,
            'mchid'        => $this->mchid,
            'description'  => $description,
            'out_trade_no' => $trade_no,
            'notify_url'   => $notify_url,
            'time_expire'  => $expire,
            'amount'       => ['total' => $money * 100, 'currency' => 'CNY',],
            'payer'        => ['openid' => $openid,],
        ];
        $json      = json_encode($body);
        $headers   = $this->sign('POST', $apiUrl, $json);
        $resultXml = $this->curlPost($apiUrl, $json, $headers);


        // 解析返回的参数
        $result = json_decode($resultXml, true);
        if(empty($result)){
            $result = $this->xmlToArray($resultXml);
        }

        return $result;
    }

    // 退款操作
    public function refund($old_trade_no, $orderNo, $money, $notify_url)
    {
        // 订单退款操作
        $apiUrl = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';//退单接口
        $body   = [
            //'transaction_id' => $transaction_id,   //  原支付交易对应的微信订单号，与out_trade_no二选一
            'out_trade_no'  => $old_trade_no,     //  原支付交易对应的商户订单号，与transaction_id二选一
            'out_refund_no' => $orderNo,  //系统退款单号
            'reason'        => '退款',  //退款原因
            'notify_url'    => $notify_url,   //退款回调
            'amount'        => [
                'refund'   => $money * 100,  //退款金额
                'total'    => $money * 100,  //原订单金额
                'currency' => 'CNY',
            ],
        ];
        $json   = json_encode($body);

        // 制作 header
        $headers = $this->sign('POST', $apiUrl, $json);
        $result  = $this->curlPost($apiUrl, $json, $headers);

        return $result;
    }


    //  微信提现：转账到零钱
    //    这里只能为一个人提现
    public function WechatTransfer($description, $orderNo, $openid, $money, $notify_url)
    {
        $params = [
            'appid'                => $this->mini_appid,
            'mch_id'               => $this->mchid,
            'out_batch_no'         => $orderNo,
            'batch_name'           => '用户提现',
            'batch_remark'         => $description.date('Y-m-d H:i:s'),
            'total_amount'         => $money * 100,
            'total_num'            => 1,  // 只对一个人进行转账
            'notify_url'           => $notify_url,
            // 接收转账的人的信息
            'transfer_detail_list' => [
                [
                    //单次转账一人，多人添加数组即可
                    'out_detail_no'   => $orderNo.'N1',  // 第一个人   中间不能是下划线，只能是字母
                    'transfer_remark' => $description,
                    'transfer_amount' => $money * 100,
                    'openid'          => $openid,
                ],
            ],
        ];

        ksort($params);
        reset($params);


        $apiUrl = 'https://api.mch.weixin.qq.com/v3/transfer/batches';//微信提现接口
        $json   = json_encode($params);
        //$token  = $this->getToken('POST', $apiUrl, $json); //获取header Token认证

        // 制作 header
        $headers = $this->sign('POST', $apiUrl, $json);
        $result  = $this->curlPost($apiUrl, $json, $headers);

        return $result;
    }


    // 组合给前端调取支付： 注意参数大小写
    public function toJsPay($timeStamp, $prepayid)
    {
        $appId   = $this->mini_appid;
        $noceStr = $this->noceStr($timeStamp);
        $pay     = ["timeStamp" => $timeStamp, "nonceStr" => $noceStr, "package" => "prepay_id=".$prepayid, "signType" => "RSA",];

        //构造签名串
        $message = $appId."\n".
                   $timeStamp."\n".
                   $noceStr."\n".
                   "prepay_id=".$prepayid."\n";//报文主体

        //计算签名值
        $pay["paySign"] = $this->sslSign($message);

        return $pay;
    }


    /**
     * 签名：生成header
     * @param string $http_method 请求方式GET|POST
     * @param string $url url
     * @param string $body 报文主体
     * @return array
     */
    public function sign($http_method = 'POST', $url = '', $body = '')
    {
        $timestamp = time();   //时间戳
        $nonce     = $this->noceStr($timestamp);  //随机串
        $url_parts = parse_url($url);
        $_url      = ($url_parts['path'].(!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));   // 组合标准的url

        //构造签名串
        $message = $http_method."\n".
                   $_url."\n".
                   $timestamp."\n".
                   $nonce."\n".
                   $body."\n";//报文主体

        //计算签名值
        // $pri_key = $this->getMchKey();  //私钥
        //openssl_sign($message, $raw_sign, $pri_key, 'sha256WithRSAEncryption');
        //$sign = base64_encode($raw_sign);

        //计算签名值
        $sign = $this->sslSign($message);

        // 计算token
        $token = sprintf(
            'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $this->mchid,
            $nonce,
            $timestamp,
            $this->serial_no,
            $sign
        );

        // 设置HTTP头
        $headers = [
            'Accept: application/json',
            'User-Agent: */*',
            'Content-Type: application/json; charset=utf-8',
            'Authorization: '.$token,
            //'User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
        ];

        return $headers;
    }


    // 从证书中获取商户序列号 serial_no
    //  序列号：7DC5F002FBA35D0B13420BAA49B8AFE7BF17C62B 可以自己填
    public function merchantSerialNo()
    {
        // $client_cert_path = $this->cert_pem;  //商户证书
        // $client_key_path  = $this->key_pem;   //商户密匙
        $client_cert_path = '../app/Service/WeChat/cert/apiclient_cert.pem';  //商户证书
        $stream_opts      = ["ssl" => ["verify_peer" => false, "verify_peer_name" => false,],];
        $client_cert_arr  = openssl_x509_parse(file_get_contents($client_cert_path, false, stream_context_create($stream_opts)));
        $serial_no        = $client_cert_arr['serialNumberHex'];//证书序列号(忽略)

        return $serial_no;
    }

    //post请求
    public function curlPost($url, $data, $headers = array())
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        //设置header头
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        // POST数据
        curl_setopt($ch, CURLOPT_POST, 1);
        // 把post的变量加上
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        $output = curl_exec($ch);
        curl_close($ch);

        return $output;
    }

    //get请求
    public function curlGet($url, $headers = array())
    {
        $info = curl_init();
        curl_setopt($info, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($info, CURLOPT_HEADER, 0);
        curl_setopt($info, CURLOPT_NOBODY, 0);
        curl_setopt($info, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($info, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($info, CURLOPT_SSL_VERIFYHOST, false);
        //设置header头
        curl_setopt($info, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($info, CURLOPT_URL, $url);
        $output = curl_exec($info);
        curl_close($info);

        return $output;
    }


    //  打印log
    public function logwx($type, $msg = '')
    {
        if($msg){
            $msg = is_string($msg) ? $msg : json_encode($msg);
        }

        // 保存到文件中
        $file = app()->storagePath().'/logwx_'.date('Y-m-d').'.txt';
        $msg  = date('Y-m-d H:i:s').'  '.$type.':'.$msg."\r\n";
        file_put_contents($file, $msg, FILE_APPEND);
    }


    /**
     * [verifySign 验证签名]
     * @param  [type] $data   [description]
     * @param  [type] $sign   [description]
     * @param  [type] $serial [description]
     * @return [type]         [description]
     */
    public function verifySign($data, $sign)
    {
        if(!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))){
            throw new \RuntimeException("当前PHP环境不支持SHA256withRSA");
        }

        $sign = \base64_decode($sign);
        // 拼接生成签名所需的字符串
        $message = '';
        foreach($data as $value){
            $message .= (is_array($value) ? json_encode($value) : $value)."\n";
        }

        // 平台公钥
        $public_key = $this->getPublicKey(); //平台公钥


        // 验证签名
        $recode = \openssl_verify($message, $sign, $public_key, 'sha256WithRSAEncryption');

        return $recode == 1 ? true : false;
    }


    /**
     * [decryptToString 证书和回调报文解密]
     * @param  [type] $associatedData [附加数据包（可能为空）]
     * @param  [type] $nonceStr       [加密使用的随机串初始化向量]
     * @param  [type] $ciphertext     [Base64编码后的密文]
     * @return [type]                 [description]
     */
    public function decryptToString($associatedData, $nonceStr, $ciphertext)
    {
        $api3key    = $this->api3_key;
        $ciphertext = base64_decode($ciphertext);
        if(strlen($ciphertext) <= 16){
            return false;
        }

        // ext-sodium (default installed on >= PHP 7.2)
        if(function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()){
            return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $api3key);
        }

        // ext-libsodium (need install libsodium-php 1.x via pecl)
        if(function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()){
            return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $api3key);
        }

        // openssl (PHP >= 7.1 support AEAD)
        if(PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())){
            $ctext   = substr($ciphertext, 0, -16);
            $authTag = substr($ciphertext, -16);

            return \openssl_decrypt(
                $ctext,
                'aes-256-gcm',
                $api3key,
                \OPENSSL_RAW_DATA,
                $nonceStr,
                $authTag,
                $associatedData
            );
        }

        throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }


    // 根据证书文件 加密
    protected function sslSign($message)
    {
        $pri_key = $this->getMchKey();  //私钥
        //计算签名值
        openssl_sign($message, $raw_sign, $pri_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);

        return $sign;
    }

    // 根据证书文件 加密
    protected function getEncrypt($message)
    {
        $public_key_path = '平台证书路径';
        $public_key      = file_get_contents($public_key_path);
        $encrypted       = '';
        if(openssl_public_encrypt($message, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)){
            //base64编码
            $sign = base64_encode($encrypted);
        } else{
            throw new Exception('encrypt failed');
        }

        return $sign;
    }

    // 获取token
    protected function getToken($http_method, $url, $body = '')
    {
        // $url = 'https://api.mch.weixin.qq.com/v3/certificates';


        //$serial_no = $this->merchantSerialNo();
        //$pri_key   = $this->getMchKey2();

        $pri_key   = $this->getMchKey();  //私钥
        $serial_no = $this->serial_no;

        $timestamp = time();   //时间戳
        $url_parts = parse_url($url);//获取请求的绝对URL
        $nonce     = $timestamp.rand('10000', '99999');//请求随机串
        $_url      = ($url_parts['path'].(!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $message   = $http_method."\n".
                     $_url."\n".
                     $timestamp."\n".
                     $nonce."\n".
                     $body."\n";

        openssl_sign($message, $raw_sign, $pri_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);//签名


        $schema = 'WECHATPAY2-SHA256-RSA2048';
        $token  = sprintf(
            $schema.' mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $this->mchid,
            $nonce,
            $timestamp,
            $serial_no,
            $sign
        );  //微信返回token

        return $token;
    }


    // xml转为数组
    protected function xmlToArray($xmlStr)
    {
        $postStr = $xmlStr;
        $msg     = (array)simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);

        return $msg;
    }


    // 私钥，这里需要手动防止好私钥证书，并且需要修改下存放的位置
    protected function getMchKey()
    {
        //path->私钥文件存放路径
        $client_key_path = '../app/Service/WeChat/cert/apiclient_key.pem';  //商户密匙

        return openssl_get_privatekey(file_get_contents($client_key_path));
    }

    // 获取平台公钥
    public function getPublicKey()
    {
        $public_key_path = '../app/Service/WeChat/cert/public_key.pem';  //获取公钥
        if(!is_file($public_key_path)){
            // 请求接口获取证书信息
            $apiUrl = 'https://api.mch.weixin.qq.com/v3/certificates';
            $header = $this->sign('GET', $apiUrl);
            $result = $this->curlGet($apiUrl, $header);
            $result = json_decode($result, true);
            if(empty($result['data'])){
                throw new \RuntimeException("[".$result['code']."] ".$result['message']);
            }

            // 保存证书信息
            // 平台证书序列号
            $serial = '3896D5569B26A2894CEE4D190E602E14B4985692';
            $serial = Config::get('wx.serial_platform');
            foreach($result['data'] as $key => $certificate){
                if($certificate['serial_no'] == $serial){
                    $encrypt   = $certificate['encrypt_certificate'];
                    $publicKey = $this->decryptToString($encrypt['associated_data'], $encrypt['nonce'], $encrypt['ciphertext']);
                    file_put_contents($public_key_path, $publicKey);
                    break;
                }
            }
        }
        if(!is_file($public_key_path)){
            throw new \RuntimeException("微信支付平台公钥证书文件不存在！");
        }

        $public_key = openssl_get_publickey(file_get_contents($public_key_path));

        //$public_key2 = openssl_pkey_get_public(file_get_contents($public_key_path));

        return $public_key;
    }


    // 私钥，这里需要手动防止好私钥证书，并且需要修改下存放的位置
    protected function getMchKey2()
    {
        //path->私钥文件存放路径
        //$client_key_path  = $this->key_pem;  //商户密匙
        $client_key_path = '';  //商户密匙
        $stream_opts     = ["ssl" => ["verify_peer" => false, "verify_peer_name" => false,],];
        $mch_private_key = file_get_contents($client_key_path, false, stream_context_create($stream_opts));//密钥

        return $mch_private_key;
    }


    // 获取随机字符串
    protected function noceStr($timeStamp)
    {
        return md5(rand(100, 1000).$timeStamp);
    }

    //  获得随机字符串
    protected function getRandomStr($len, $special = false)
    {
        $chars = array(
            "a",
            "b",
            "c",
            "d",
            "e",
            "f",
            "g",
            "h",
            "i",
            "j",
            "k",
            "l",
            "m",
            "n",
            "o",
            "p",
            "q",
            "r",
            "s",
            "t",
            "u",
            "v",
            "w",
            "x",
            "y",
            "z",
            "A",
            "B",
            "C",
            "D",
            "E",
            "F",
            "G",
            "H",
            "I",
            "J",
            "K",
            "L",
            "M",
            "N",
            "O",
            "P",
            "Q",
            "R",
            "S",
            "T",
            "U",
            "V",
            "W",
            "X",
            "Y",
            "Z",
            "0",
            "1",
            "2",
            "3",
            "4",
            "5",
            "6",
            "7",
            "8",
            "9",
        );

        if($special){
            $chars = array_merge($chars, array(
                "!",
                "@",
                "#",
                "$",
                "?",
                "|",
                "{",
                "/",
                ":",
                ";",
                "%",
                "^",
                "&",
                "*",
                "(",
                ")",
                "-",
                "_",
                "[",
                "]",
                "}",
                "<",
                ">",
                "~",
                "+",
                "=",
                ",",
                ".",
            ));
        }

        $charsLen = count($chars) - 1;
        shuffle($chars);                            //打乱数组顺序
        $str = '';
        for($i = 0; $i < $len; $i++){
            $str .= $chars[mt_rand(0, $charsLen)];    //随机取出一位
        }

        return $str;
    }


    ##############################################################################################################


    // 新版相关接口
    // 统一下订单管理
    private static $transactionsUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/';
    // 申请退款
    private static $refundUrl = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
    // 静默授权，获取code
    private static $authorizeUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize';
    // 通过code获取access_token以及openid
    private static $accessTokenUrl = 'https://api.weixin.qq.com/sns/oauth2/access_token';

    // 支付完整配置
    private static $config = array(
        'xcxid'     => '', // 小程序appid
        'appid'     => '', // 微信支付appid
        'mchid'     => '', // 微信支付 mch_id 商户收款账号
        'key'       => '', // 微信支付 apiV3key（尽量包含大小写字母，否则验签不通过）
        'appsecret' => '', // 公众帐号 secert (公众号支付获取code 和 openid使用)

        'notify_url'   => '', // 接收支付状态的连接  改成自己的回调地址
        'redirect_url' => '', // 公众号支付，调起支付页面

        'serial_no'   => '', // 证书序列号
        'cert_client' => './cert/apiclient_cert.pem', // 证书（退款，红包时使用）
        'cert_key'    => './cert/apiclient_key.pem', // 商户私钥（Api安全中下载）
        'public_key'  => './cert/public_key.pem', // 平台公钥（调动证书列表，自动生成）
    );


    /**
     * [unifiedOrder 统一下单]
     * @param  [type]  $order [订单信息（必须包含支付所需要的参数）]
     * @param boolean $type [区分是否是小程序，是则传 true]
     * @return [type]         [description]
     * $order = array(
     *      'body'         => '', // 产品描述
     *      'order_sn'     => '', // 订单编号
     *      'total_amount' => '', // 订单金额（分）
     * );
     */
    public static function unifiedOrder($order, $type = false)
    {
        $config = array_filter(self::$config);

        // 获取配置项
        $params = array(
            'appid'        => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的应用ID
            'mchid'        => $config['mchid'], // 直连商户的商户号
            'description'  => $order['body'], // 商品描述
            'out_trade_no' => (string)$order['order_sn'], // 商户系统内部订单号
            'notify_url'   => $config['notify_url'], // 通知URL必须为直接可访问的URL
            'amount'       => ['total' => (int)$order['total_amount'], 'currency' => 'CNY'], // 订单金额信息
        );

        !empty($order['attach']) && $params['attach'] = $order['attach']; // 附加数据

        // 订单失效时间
        if(!empty($order['time_expire'])){
            preg_match('/[年\/-]/', $order['time_expire']) && $order['time_expire'] = strtotime($order['time_expire']);

            $time                  = $order['time_expire'] > time() ? $order['time_expire'] : $order['time_expire'] + time();
            $params['time_expire'] = date(DATE_ATOM, $time);
        }

        if(!in_array($order['type'], ['native'])){
            !empty($order['openid']) && $params['payer'] = ['openid' => $order['openid']];
            $params['scene_info'] = ['payer_client_ip' => self::get_ip()];
        }

        if(in_array($order['type'], ['iOS', 'Android', 'Wap'])){
            $params['scene_info']['h5_info'] = ['type' => $order['type']];

            $url = self::$transactionsUrl.'h5'; // 拼接请求地址
        } else{
            $url = self::$transactionsUrl.strtolower($order['type']); // 拼接请求地址
        }

        $header   = self::createAuthorization($url, $params, 'POST');
        $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header);
        $result   = json_decode($response, true);
        if(isset($result['code']) && isset($result['message'])){
            throw new \Exception("[".$result['code']."] ".$result['message']);
        }

        return $result;
    }

    /**
     * [query 查询订单]
     * @param  [type]  $orderSn [订单编号]
     * @param boolean $type [微信支付订单编号，是否是微信支付订单号]
     * @return [type]           [description]
     */
    public static function query($orderSn, $type = false)
    {
        $config = self::$config;
        $url    = self::$transactionsUrl.($type ? 'id/' : 'out-trade-no/').$orderSn.'?mchid='.$config['mchid'];
        $params = '';

        $header   = self::createAuthorization($url, $params, 'GET');
        $response = Http::get($url, $params, $header);
        $result   = json_decode($response, true);

        return $result;
    }

    /**
     * [close 关闭订单]
     * @param  [type] $orderSn [微信支付订单编号]
     * @return [type]          [description]
     */
    public static function close($orderSn)
    {
        $config          = self::$config;
        $url             = self::$transactionsUrl.'out-trade-no/'.$orderSn.'/close';
        $params['mchid'] = $config['mchid'];

        $header   = self::createAuthorization($url, $params, 'POST');
        $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header);
        $result   = json_decode($response, true);

        return true;
    }

    /**
     * [js 获取jssdk需要用到的数据]
     * @param  [type] $order [订单信息数组]
     * @return [type]        [description]
     */
    public static function js($order = [])
    {
        $config = self::$config;
        if(!is_array($order) || count($order) < 3)
            die("订单数组信息缺失！");

        if(count($order) == 4 && !empty($order['openid'])){
            $data = self::xcx($order, false, false); // 获取支付相关信息(获取非小程序信息)

            return $data;
        }

        $code        = !empty($order['code']) ? $order['code'] : ($_GET['code'] ?? '');
        $redirectUri = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].rtrim($_SERVER['REQUEST_URI'], '/').'/'; // 重定向地址
        $params      = ['appid' => $config['appid']];
        // 如果没有get参数没有code；则重定向去获取code；
        if(empty($code)){
            $params['redirect_uri']  = $redirectUri; // 返回的url
            $params['response_type'] = 'code';
            $params['scope']         = 'snsapi_base';
            $params['state']         = $order['order_sn']; // 获取订单号

            $url = self::$authorizeUrl.'?'.http_build_query($params).'#wechat_redirect';
        } else{
            $params['secret']     = $config['appsecret'];
            $params['code']       = $code;
            $params['grant_type'] = 'authorization_code';

            $response = Http::get(self::$accessTokenUrl, $params); // 进行GET请求
            $result   = json_decode($response, true);

            $order['openid'] = $result['openid']; // 获取到的openid
            $data            = self::xcx($order, false, false); // 获取支付相关信息(获取非小程序信息)

            if(!empty($order['code'])){
                return $data;
            }

            $url = $config['redirect_url'] ?? $redirectUri;
            $url .= '?data='.json_encode($data, JSON_UNESCAPED_UNICODE);
        }
        header('Location: '.$url);
        die;
    }

    /**
     * [app 获取APP支付需要用到的数据]
     * @param  [type]  $order [订单信息数组]
     * @return [type]         [description]
     */
    public static function app($order = [], $log = false)
    {
        if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){
            die("订单数组信息缺失！");
        }

        $order['type'] = 'app'; // 获取订单类型，用户拼接请求地址
        $result        = self::unifiedOrder($order, true);
        if(!empty($result['prepay_id'])){
            $data              = array(
                'appId'     => self::$config['appid'], // 微信开放平台审核通过的移动应用appid
                'timeStamp' => (string)time(),
                'nonceStr'  => self::get_rand_str(32, 0, 1), // 随机32位字符串
                'prepayid'  => $result['prepay_id'],
            );
            $data['paySign']   = self::makeSign($data);
            $data['partnerid'] = $config['mchid'];
            $data['package']   = 'Sign=WXPay';

            return $data; // 数据小程序客户端
        } else{
            return $log ? $result : false;
        }
    }

    /**
     * [h5 微信H5支付]
     * @param  [type] $order [订单信息数组]
     * @return [type]        [description]
     */
    public static function h5($order = [], $log = false)
    {
        if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['type']) || !in_array(strtolower($order['type']), ['ios', 'android', 'wap'])){
            die("订单数组信息缺失！");
        }
        $result = self::unifiedOrder($order);
        if(!empty($result['h5_url'])){
            return $result['h5_url']; // 返回链接让用户点击跳转
        } else{
            return $log ? $result : false;
        }
    }

    /**
     * [xcx 获取jssdk需要用到的数据]
     * @param  [type]  $order [订单信息数组]
     * @param boolean $log [description]
     * @param boolean $type [区分是否是小程序，默认 true]
     * @return [type]         [description]
     */
    public static function xcx($order = [], $log = false, $type = true)
    {
        if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['openid'])){
            die("订单数组信息缺失！");
        }

        $order['type'] = 'jsapi'; // 获取订单类型，用户拼接请求地址
        $config        = self::$config;
        $result        = self::unifiedOrder($order, $type);
        if(!empty($result['prepay_id'])){
            $data             = array(
                'appId'     => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的应用ID
                'timeStamp' => (string)time(),
                'nonceStr'  => self::get_rand_str(32, 0, 1), // 随机32位字符串
                'package'   => 'prepay_id='.$result['prepay_id'],
            );
            $data['paySign']  = self::makeSign($data);
            $data['signType'] = 'RSA';

            return $data; // 数据小程序客户端
        } else{
            return $log ? $result : false;
        }
    }

    /**
     * [scan 微信扫码支付]
     * @param  [type] $order [订单信息数组]
     * @return [type]        [description]
     */
    public static function scan($order = [], $log = false)
    {
        if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){
            die("订单数组信息缺失！");
        }
        $order['type'] = 'native'; // Native支付
        $result        = self::unifiedOrder($order);

        if(!empty($result['code_url'])){
            return urldecode($result['code_url']); // 返回链接让用户点击跳转
        } else{
            return $log ? $result : false;
        }
    }

    /**
     * [notify 回调验证]
     * @return [array] [返回数组格式的notify数据]
     */
    public static function notify($server = [], $response = [])
    {
        $config   = self::$config;
        $server   = $server ?? $_SERVER;
        $response = $response ?? file_get_contents('php://input', 'r');
        if(empty($response) || empty($server['HTTP_WECHATPAY_SIGNATURE'])){
            return false;
        }
        $body = [
            'timestamp' => $server['HTTP_WECHATPAY_TIMESTAMP'],
            'nonce'     => $server['HTTP_WECHATPAY_NONCE'],
            'data'      => $response,
        ];
        // 验证应答签名
        $verifySign = self::verifySign($body, trim($server['HTTP_WECHATPAY_SIGNATURE']), trim($server['HTTP_WECHATPAY_SERIAL']));
        if(!$verifySign){
            die("签名验证失败！");
        }
        $result = json_decode($response, true);
        if(empty($result) || $result['event_type'] != 'TRANSACTION.SUCCESS' || $result['summary'] != '支付成功'){
            return false;
        }
        // 加密信息
        $associatedData = $result['resource']['associated_data'];
        $nonceStr       = $result['resource']['nonce'];
        $ciphertext     = $result['resource']['ciphertext'];
        $data           = $result['resource']['ciphertext'] = self::decryptToString($associatedData, $nonceStr, $ciphertext);

        return json_decode($data, true);
    }

    /**
     * [refund 微信支付退款]
     * @param  [type] $order [订单信息]
     * @param  [type] $type  [是否是小程序]
     */
    public static function refund2($order)
    {
        $config = self::$config;
        if(empty($order['refund_sn']) || empty($order['refund_amount']) || (empty($order['order_sn']) && empty($order['transaction_id']))){
            die("订单数组信息缺失！");
        }

        $params = array(
            'out_refund_no' => (string)$order['refund_sn'], // 商户退款单号
            'funds_account' => 'AVAILABLE', // 退款资金来源
            'amount'        => [
                'refund'   => $order['refund_amount'],
                'currency' => 'CNY',
            ],
        );

        if(!empty($order['transaction_id'])){
            $params['transaction_id'] = $order['transaction_id'];
            $orderDetail              = self::query($order['transaction_id'], true);
        } else{
            $params['out_trade_no'] = $order['order_sn'];
            $orderDetail            = self::query($order['order_sn']);
        }
        $params['amount']['total'] = $orderDetail['amount']['total'];

        !empty($order['reason']) && $params['reason'] = $order['reason'];

        $url      = self::$refundUrl;
        $header   = self::createAuthorization($url, $params, 'POST');
        $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header);
        $result   = json_decode($response, true);

        return $result;
    }

    /**
     * [queryRefund 查询退款]
     * @param  [type] $refundSn [退款单号]
     * @return [type]           [description]
     */
    public static function queryRefund($refundSn, $type = false)
    {
        $url    = self::$refundUrl.'/'.$refundSn;
        $params = '';

        $header   = self::createAuthorization($url, $params, 'GET');
        $response = Http::get($url, $params, $header);
        $result   = json_decode($response, true);

        return $result;
    }

    /**
     * [success 通知支付状态]
     */
    public static function success()
    {
        $str = ['code' => 'SUCCESS', 'message' => '成功'];
        die(json_encode($str, JSON_UNESCAPED_UNICODE));
    }

    /**
     * [createAuthorization 获取接口授权header头信息]
     * @param  [type] $url    [请求地址]
     * @param array $data [请求参数]
     * @param string $method [请求方式]
     * @return [type]         [description]
     */
    //生成v3 Authorization
    protected static function createAuthorization($url, $data = [], $method = 'POST')
    {
        $config = self::$config;
        //商户号
        $mchid = $config['mchid'];
        // 证书序列号
        if(empty($config['serial_no'])){
            $certFile  = @file_get_contents($config['cert_client']);
            $certArr   = openssl_x509_parse($publicStr);
            $serial_no = $certArr['serialNumberHex'];
        } else{
            $serial_no = $config['serial_no'];
        }

        // 解析url地址
        $url_parts = parse_url($url);
        //生成签名
        $body = [
            'method' => $method,
            'url'    => ($url_parts['path'].(!empty($url_parts['query']) ? "?${url_parts['query']}" : "")),
            'time'   => time(), // 当前时间戳
            'nonce'  => self::get_rand_str(32, 0, 1), // 随机32位字符串
            'data'   => (strtolower($method) == 'post' ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data), // POST请求时 需要 转JSON字符串
        ];
        $sign = self::makeSign($body);
        //Authorization 类型
        $schema = 'WECHATPAY2-SHA256-RSA2048';
        //生成token
        $token = sprintf(
            'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $mchid,
            $body['nonce'],
            $body['time'],
            $serial_no,
            $sign
        );

        $header = [
            'Content-Type:application/json',
            'Accept:application/json',
            'User-Agent:*/*',
            'Authorization: '.$schema.' '.$token,
        ];

        return $header;
    }

    /**
     * [makeSign 生成签名]
     * @param  [type] $data [加密数据]
     * @return [type]       [description]
     */
    public static function makeSign($data)
    {
        $config = self::$config;
        if(!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))){
            throw new \RuntimeException("当前PHP环境不支持SHA256withRSA");
        }
        // 拼接生成签名所需的字符串
        $message = '';
        foreach($data as $value){
            $message .= $value."\n";
        }
        // 商户私钥
        $private_key = self::getPrivateKey(
            $config['cert_key']
        );
        // 生成签名
        openssl_sign($message, $sign, $private_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($sign);

        return $sign;
    }

    //获取私钥
    public static function getPrivateKey($filepath)
    {
        return openssl_pkey_get_private(file_get_contents($filepath));
    }


    /** fengkui.net
     * [get_rand_str 获取随机字符串]
     * @param integer $randLength [长度]
     * @param integer $addtime [是否加入当前时间戳]
     * @param integer $includenumber [是否包含数字]
     * @return [type]                 [description]
     */
    public static function get_rand_str($randLength = 6, $addtime = 0, $includenumber = 1)
    {
        if($includenumber)
            $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
        $chars = 'abcdefghijklmnopqrstuvwxyz';

        $len     = strlen($chars);
        $randStr = '';
        for($i = 0; $i < $randLength; $i++){
            $randStr .= $chars[rand(0, $len - 1)];
        }
        $tokenvalue = $randStr;
        $addtime && $tokenvalue = $randStr.time();

        return $tokenvalue;
    }

    /** fengkui.net
     * [get_ip 定义一个函数get_ip() 客户端IP]
     * @return [type] [description]
     */
    public static function get_ip()
    {
        if(getenv("HTTP_CLIENT_IP"))
            $ip = getenv("HTTP_CLIENT_IP");
        elseif(getenv("HTTP_X_FORWARDED_FOR"))
            $ip = getenv("HTTP_X_FORWARDED_FOR");
        elseif(getenv("REMOTE_ADDR"))
            $ip = getenv("REMOTE_ADDR");
        else $ip = "Unknow";

        if(preg_match('/^((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1 -9]?\d))))$/', $ip))
            return $ip;
        else
            return '';
    }

}


